#include <alloc.h>
#include <bios.h>
#include <mem.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "COMMON.H"

#define GetFrontBuffer() (1 - BackBuffer)

#define DIRTY_LIST_LENGTH 64

typedef struct
{
	short Count;
	Rect Rects[DIRTY_LIST_LENGTH];
} DirtyList;

byte Palette[256 * 3];
byte FileBuffer[1024];
bool EnableSound;
bool OPL3;
bool KeepRunning;
short MouseX, MouseY;
bool MouseLeft, MouseLeftOld, MouseRight, MouseRightOld, ShowMouse;
Screen *BootScreen, *DesktopScreen, *GameScreen, *InstructionsScreen, *ShutdownScreen;
Screen *ActiveScreen;
DirtyList DirtyLists[2];
short BackBuffer;
Screen *ScreenToActivate;
short WinCount;

void ActivateScreen(Screen *screen)
{
	ScreenToActivate = screen;
}

void Quit()
{
	KeepRunning = false;
}

void CleanUp(void)
{
	// Don't leave the speaker on:
	nosound();

	// Restore text mode:
	SetVideoMode(0x03);

	if (OPL3)
	{
		ShutdownOPL3();
	}
}

void Assert(bool condition, char *message)
{
	if (!condition)
	{
		FILE* f;
		CleanUp();
		if (!message) message = "(no message)";
		f = fopen("SHENZHEN.ERR", "w");
		fprintf(f, "Error: %s\n", message);
		fclose(f);
		exit(1);
	}
}

bool RectContains(Rect *rect, short x, short y)
{
	return x >= rect->X && x < (rect->X + rect->W) &&
		y >= rect->Y && y < (rect->Y + rect->H);
}

bool RectOverlaps(Rect *firstRect, Rect *secondRect)
{
	return !(firstRect->X + firstRect->W < secondRect->X ||
		firstRect->X > secondRect->X + secondRect->W ||
		firstRect->Y + firstRect->H < secondRect->Y ||
		firstRect->Y > secondRect->Y + secondRect->H);
}

short Min(short a, short b)
{
	return (a < b) ? a : b;
}

short Max(short a, short b)
{
	return (a > b) ? a : b;
}

void PlayClick(void)
{
	byte x = inp(0x61) & 0xFC;
	outp(0x61, x);
	_delay(1);
	outp(0x61, x | 0x02);
	_delay(1);
	nosound();
}

void LoadWinCount(void)
{
	FILE *file;

	WinCount = 0;

	file = fopen("SHENZHEN.SAV", "rb");
	if (file != 0)
	{
		fread(&WinCount, sizeof(WinCount), 1, file);
		fclose(file);
	}
}

void SaveWinCount(void)
{
	FILE *file = fopen("SHENZHEN.SAV", "wb");
	fwrite(&WinCount, sizeof(WinCount), 1, file);
	fclose(file);
}

void DirtyList_Add(DirtyList *list, short x, short y, short w, short h)
{
	short i;

	// If this rectangle is fully inside a previous rectangle, don't add it:
	for (i = 0; i < list->Count; i++)
	{
		Rect rect = list->Rects[i];
		if (x >= rect.X &&
			x + w <= rect.X + rect.W &&
			y >= rect.Y &&
			y + h <= rect.Y + rect.H)
		{
			return;
		}
	}

	// Add this rectangle to the list:
	Assert(list->Count < DIRTY_LIST_LENGTH, "dirty list full");
	list->Rects[list->Count].X = x;
	list->Rects[list->Count].Y = y;
	list->Rects[list->Count].W = w;
	list->Rects[list->Count].H = h;
	list->Count += 1;
}

void DirtyList_Clear(DirtyList *list)
{
	list->Count = 0;
}

void MarkRectDirty(short x, short y, short w, short h)
{
	DirtyList_Add(&DirtyLists[0], x, y, w, h);
	DirtyList_Add(&DirtyLists[1], x, y, w, h);
}

int main(int argc, char *argv[])
{
	union REGS regs;
	short i;
	byte huge *h;
	long usec;

	srand(time(NULL));

	if (argc >= 2 && !stricmp(argv[1], "/NOMUSIC"))
	{
		OPL3 = false;
	}
	else
	{
		OPL3 = true;
		InitOPL3();
	}

	// Reset the mouse driver to give DOSBox a hint that the mouse must be captured.
	regs.x.ax = 0;
	int86(0x33, &regs, &regs);

	// Read the palette.
	{
		FILE *f;
		long size, loaded;

		f = fopen("SHENZHEN.PAL", "rb");
		Assert(f != NULL, "error reading palette");
		size = sizeof(Palette);
		loaded = fread(&Palette, 1, size, f);
		if (loaded != size)
		{
			fprintf(stderr, "Error reading palette %ld.\n", loaded);
			exit(1);
		}
		fclose(f);
	}

	SetVideoModeX();
	SetPalette(Palette);
	SetDisplayedScreen(0);

	// Draw the loading screen background:
	SetTargetScreen(0);
	FillRect(0, 0, 0, 320, 240);
	FillRect(2, 101, 141, 112, 5);
	_delay(20);

	// Read the image data.
	{
		FILE *f;
		short result, progress;
		long size, loaded;
		byte huge *dest;
		bool logoDrawn;

		f = fopen("SHENZHEN.IMG", "rb");
		Assert(f != NULL, "error reading images");
		fseek(f, 0, SEEK_END);
		size = ftell(f);
		fseek(f, 0, SEEK_SET);
		ImageData = farmalloc(size);
		Assert(ImageData != NULL, "error loading images");
		// Read image data in small pieces, then copy it to the
		// far image data buffer.
		loaded = 0;
		dest = ImageData;
		logoDrawn = false;
		while (loaded < size)
		{
			// Slow down the loading process artificially:
			_delay(5);
			result = fread(&FileBuffer, 1, sizeof(FileBuffer), f);
			if (result == 0)
			{
				fprintf(stderr, "Error reading image data.\n");
				exit(1);
			}
			_fmemcpy(dest, &FileBuffer, result);
			loaded += result;
			dest += result;

			// Once the boot screen background has loaded, draw it:
			if (!logoDrawn && loaded > ImageOffsets[IMG_BOOT_BACKGROUND + 1])
			{
				Blit(IMG_BOOT_BACKGROUND, 101, 100);
				logoDrawn = true;
			}

			// Update the loading bar:
			progress = Min(112, 112 * loaded / size);
			FillRect(3, 101, 141, progress, 5);
		}
		fclose(f);
	}

	if (OPL3)
		LoadSong("SHENZHEN.DRO");

	// Load the saved win count.
	LoadWinCount();

	DesktopScreen = InitDesktopScreen();
	GameScreen = InitGameScreen();
	InstructionsScreen = InitInstructionsScreen();
	ShutdownScreen = InitShutdownScreen();

	ActivateScreen(DesktopScreen);
	KeepRunning = true;
	while (KeepRunning)
	{
		SetTargetScreen(2);

		// Read the mouse information:
		regs.x.ax = 0x03;
		int86(0x33, &regs, &regs);
		// Mouse position is reported in 640x200 resolution;
		//   convert to Mode X resolution: 320x240.
		MouseX = regs.x.cx / 2;
		MouseY = Min((regs.x.dx * 5) / 4, 239);
		MouseLeft = (regs.x.bx & 0x1) != 0;
		MouseRight = (regs.x.bx & 0x2) != 0;

		// Don't process input or updates on the first frame of a new cycle:
		if (ScreenToActivate != NULL)
		{
			ActiveScreen = ScreenToActivate;
			if (ActiveScreen->OnActivate != NULL)
			{
				ActiveScreen->OnActivate();
			}

			ScreenToActivate = NULL;
		}
		else
		{
			bool keyPressed = (bioskey(1) != 0);
			if (keyPressed)
			{
				// Always retrieve the key event so that it doesn't stay in the event queue.
				short event = bioskey(0);
				if (ActiveScreen->OnKeyDown != NULL)
				{
					ActiveScreen->OnKeyDown(event & 0xFF);
				}
			}

			if (ShowMouse && !MouseLeftOld && MouseLeft && ActiveScreen->OnMouseDown != NULL)
			{
				ActiveScreen->OnMouseDown();
			}
			else if (ShowMouse && MouseLeftOld && !MouseLeft && ActiveScreen->OnMouseUp != NULL)
			{
				ActiveScreen->OnMouseUp();
			}

			ActiveScreen->OnUpdate();
		}

		// Update the dirty regions of the current back-buffer:
		for (i = 0; i < DirtyLists[BackBuffer].Count; i++)
		{
			Rect rect = DirtyLists[BackBuffer].Rects[i];
			CopyScreenRect(BackBuffer, 2, rect.X, rect.Y, rect.W, rect.H);
		}
		
		DirtyList_Clear(&DirtyLists[BackBuffer]);

		// Draw the cursor and any dragged content:
		if (ShowMouse)
		{
			// HACK: The content we're drawing here will only go into the back-buffer,
			// but our calls to mark dirty rects will dirty them for the front-buffer
			// as well. Prevent any of these calls from dirtying the front-buffer by 
			// snapshotting the count and resetting to that value after.
			short frontDirtyCount = DirtyLists[GetFrontBuffer()].Count;

			SetTargetScreen(BackBuffer);
			ActiveScreen->DrawDragContent();
			BlitBlend(IMG_CURSOR, MouseX, MouseY);

			DirtyLists[GetFrontBuffer()].Count = frontDirtyCount;
		}
		
		// Swap the front-buffer with the back-buffer:
		SetDisplayedScreen(BackBuffer);
		BackBuffer = GetFrontBuffer();

		MouseLeftOld = MouseLeft;
		MouseRightOld = MouseRight;
	}

	CleanUp();
	return 0;
}
